<?php
class RtmApiException extends Exception {}

class RtmApi {
    const AUTH_URL = "http://www.rememberthemilk.com/services/auth/";
    const REST_URL = "http://api.rememberthemilk.com/services/rest/";
    const TIMEOUT = 120;
    
    const PERMS_READ = "read";
    const PERMS_WRITE = "write";
    const PERMS_DELETE = "delete";
    
    private $aNeedAuth = array ("rtm.timelines.create", "rtm.tasks.add", "rtm.tasks.delete", "rtm.tasks.notes.add", "rtm.tasks.getList",
                                "rtm.lists.getList", "rtm.tasks.complete", "rtm.tasks.delete", "rtm.tasks.postpone", "rtm.tasks.addTags",
                                "rtm.tasks.removeTags", "rtm.tasks.moveTo");
    
    private $aNeedTimeline = array ("rtm.tasks.add", "rtm.tasks.delete", "rtm.tasks.notes.add", "rtm.tasks.complete", "rtm.tasks.delete",
                                    "rtm.tasks.postpone", "rtm.tasks.addTags", "rtm.tasks.removeTags", "rtm.tasks.moveTo");
    
    private $apiKey;
    private $secret;
    
    private $authToken = null;
    private $timeline = null;
    
    public function __construct($apiKey, $secret) {
        $this->apiKey = $apiKey;
        $this->secret = $secret;
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.test.echo.rtm
     */
    public function testEcho() {
        return $this->request("rtm.test.echo");
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.test.echo.rtm
     */
    public function testLogin() {
        return $this->request("rtm.test.login");
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.auth.getFrob.rtm
     * 
     * @return string frob
     */
    public function getFrob() {
        $o = $this->request("rtm.auth.getFrob");
        return $o["rsp"]["frob"];
    }
    
    public function getDesktopAuthUrl($perms, $frob) {
        $aRequest = array ("perms" => $perms, "frob" => $frob, "api_key" => $this->apiKey);
        
        $aRequest = $this->signRequest($aRequest);
        
        $aPairs = array ();
        foreach ($aRequest as $name => $value) {
            $aPairs[] = "$name=$value";
        }
        
        return $url = self::AUTH_URL . "?" . implode("&", $aPairs);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.auth.getToken.rtm
     * @param string $frob
     * @return array
     */
    public function getToken($frob) {
        $o = $this->request("rtm.auth.getToken", array ("frob" => $frob));
        return $o["rsp"]["auth"];
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.auth.checkToken.rtm
     * @param string $token
     */
    public function checkToken($token) {
        $o = $this->request("rtm.auth.checkToken", array ("auth_token" => $token));
        return $o["rsp"]["auth"];
    }
    
    public function setAuthToken($token) {
        $this->authToken = $token;
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.timelines.create.rtm
     */
    public function beginTimeline() {
        $o = $this->request("rtm.timelines.create");
        
        $this->timeline = $o["rsp"]["timeline"];
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.add.rtm
     * @param string $name
     * @param boolean $parse
     * @param int $listId
     * @return RtmApiTaskseria
     */
    public function taskAdd($name, $parse = false, $listId = null) {
        $aRequest = array ("name" => $name);
        if ($parse) {
            $aRequest["parse"] = 1;
        }
        if ($listId !== null) {
            $aRequest["list_id"] = $listId;
        }
       
        $o = $this->request("rtm.tasks.add", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.notes.add.rtm
     * @param string $title
     * @param string $text
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     */
    public function taskNoteAdd($title, $text, $listId, $taskseriesId, $taskId) {
        $aRequest = array ("note_title" => $title, "note_text" => $text, "list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId);
        
        $o = $this->request("rtm.tasks.notes.add", $aRequest);
        return $o["rsp"]["note"];
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.getList.rtm
     * 
     * @param int|null $listId The id of the list to perform an action on
     * @param string $filter If specified, only tasks matching the desired criteria are returned. See http://www.rememberthemilk.com/help/answers/search/advanced.rtm
     * @param string $lastSync An ISO 8601 formatted time value. If last_sync is provided, only tasks modified since last_sync will be returned, 
     *                         and each element will have an attribute, current, equal to last_sync.
     * @return RtmApiList[]
     */
    public function taskGetList($listId = null, $filter = null, $lastSync = null) {
        $aRequest = array ();
        if ($listId !== null) {
            $aRequest["list_id"] = $listId;
        }
        if ($filter !== null) {
            $aRequest["filter"] = $filter;
        }
        if ($lastSync !== null) {
            $aRequest["last_sync"] = $lastSync;
        }
        
        $o = $this->request("rtm.tasks.getList", $aRequest);
        
        $aList = array ();
        if (!empty($o["rsp"]["tasks"]["list"])) {
            //pp($o["rsp"]["tasks"]);
            
            if (!isset($o["rsp"]["tasks"]["list"][0])) {
                $o["rsp"]["tasks"]["list"] = array ($o["rsp"]["tasks"]["list"]);
            }
            
            foreach ($o["rsp"]["tasks"]["list"] as $list) {
                $aList[] = RtmApiList::createFromRaw($list);
            }
        }
        
        return $aList;
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.complete.rtm
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     * @return RtmApiList
     */
    public function taskComplete($listId, $taskseriesId, $taskId) {
        $aRequest = array ("list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId);
        
        $o = $this->request("rtm.tasks.complete", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.delete.rtm
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     * @return RtmApiList
     */
    public function taskDelete($listId, $taskseriesId, $taskId) {
        $aRequest = array ("list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId);
        
        $o = $this->request("rtm.tasks.delete", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.postpone.rtm
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     * @return RtmApiList
     */
    public function taskPostpone($listId, $taskseriesId, $taskId) {
        $aRequest = array ("list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId);
        
        $o = $this->request("rtm.tasks.postpone", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.addTags.rtm
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     * @param string $tags A comma delimited list of tags
     * @return RtmApiList
     */
    public function taskAddTags($listId, $taskseriesId, $taskId, $tags) {
        $aRequest = array ("list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId, "tags" => $tags);
        
        $o = $this->request("rtm.tasks.addTags", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.removeTags.rtm
     * @param int $listId
     * @param int $taskseriesId
     * @param int $taskId
     * @param string $tags A comma delimited list of tags
     * @return RtmApiList
     */
    public function taskRemoveTags($listId, $taskseriesId, $taskId, $tags) {
        $aRequest = array ("list_id" => $listId, "taskseries_id" => $taskseriesId, "task_id" => $taskId, "tags" => $tags);
        
        $o = $this->request("rtm.tasks.removeTags", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.tasks.moveTo.rtm
     * @param $fromListId
     * @param $taskseriesId
     * @param $taskId
     * @param $toListId
     * @return RtmApiList
     */
    public function taskMoveTo($fromListId, $taskseriesId, $taskId, $toListId) {
        $aRequest = array ("from_list_id" => $fromListId, "taskseries_id" => $taskseriesId, "task_id" => $taskId, "to_list_id" => $toListId);
        
        $o = $this->request("rtm.tasks.moveTo", $aRequest);
        return RtmApiList::createFromRaw($o["rsp"]["list"]);
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/methods/rtm.lists.getList.rtm
     * @return RtmApiList[]
     */
    public function listGetList() {
        $o = $this->request("rtm.lists.getList");
        
        $aList = array ();
        foreach ($o["rsp"]["lists"]["list"] as $data) {
            $aList[] = RtmApiList::createFromRaw($data);
        }
        
        return $aList;
    }
    
    private function request($method, $aRequest = array ()) {
        
        // finish request parameters
        
        $aRequest["api_key"] = $this->apiKey;
        $aRequest["method"] = $method;
        $aRequest["format"] = "json";
        
        // add auth token if needed
        
        if ($this->authToken !== null && in_array($method, $this->aNeedAuth)) {
            $aRequest["auth_token"] = $this->authToken;
        }
        
        // add timeline if needed
        if ($this->timeline !== null && in_array($method, $this->aNeedTimeline)) {
            $aRequest["timeline"] = $this->timeline;
        }
        
        // sign the request
        $aRequest = $this->signRequest($aRequest);
        
        // make cURL call
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, self::REST_URL);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $aRequest);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        $jsonResult = curl_exec($ch);
        if (($error = curl_error($ch)) || strlen($jsonResult) == 0) {
            if (strlen($jsonResult) == 0) {
                $message = "Empty JSON result";
            }
            else {
                $message = "cURL error #" . curl_errno($ch) . ": $error";
            }
            
            throw new RtmApiException($message);
        }
        curl_close($ch);
        
        // decode JSON
        
        $result = json_decode($jsonResult, true);
        if ($result === null) {
            throw new RtmApiException("Not JSON result: $jsonResult");
        }
        
        // analyze for errors
        
        if (isset($result["rsp"]) && isset($result["rsp"]["stat"]) && $result["rsp"]["stat"] == "fail") {
            throw new RtmApiException($result["rsp"]["err"]["msg"], $result["rsp"]["err"]["code"]);
        }
        
        return $result;
    }
    
    /**
     * @link http://www.rememberthemilk.com/services/api/authentication.rtm
     * @param array $aRequest
     * @return array
     */
    private function signRequest($aRequest) {
        ksort($aRequest);
        
        $s = "";
        foreach ($aRequest as $name => $value) {
            $s .= $name . $value;
        }
        
        $aRequest["api_sig"] = md5($this->secret . $s);
        
        return $aRequest;
    }
}

interface Printable {
    /**
     * @return string
     */
    public function __toString();
}

abstract class RtmApiObject {
    /**
     * @param array $data
     * @return RtmApiObject
     */
    public static function createFromRaw($data) {
        throw new BadMethodCallException("The mehtod must be overriten in the child class");
    }
    
    protected function __construct() {
    }
}

class RtmApiTaskseria extends RtmApiObject implements Printable {
    public $id;
    public $created;
    public $modified;
    public $name;
    public $source;
    public $url;
    public $location_id;
    /**
     * 
     * @var RtmApiRrule
     */
    public $rrule;
    /**
     * 
     * @var string[]
     */
    public $tags;
    public $participants;
    /**
     * 
     * @var RtmApiNote[]
     */
    public $notes;
    /**
     * 
     * @var RtmApiTask
     */
    public $task;
    
    public $listId = null;
    
    /**
     * @param array $data
     * @return RtmApiTaskseria
     */
    public static function createFromRaw($data, $listId = null) {
        $taskSeria = new RtmApiTaskseria();
        $taskSeria->id = $data["id"];
        $taskSeria->created = $data["created"];
        $taskSeria->modified = $data["modified"];
        $taskSeria->name = $data["name"];
        $taskSeria->source = $data["source"];
        $taskSeria->url = $data["url"];
        $taskSeria->location_id = $data["location_id"];
        
        $taskSeria->listId = $listId;
        
        if (isset($data["rrule"])) {
            $taskSeria->rrule = RtmApiRrule::createFromRaw($data["rrule"]);
        }
        else {
            $taskSeria->rrule = null;
        }
        
        if (isset($data["tags"]["tag"])) {
            $taskSeria->tags = !is_string($data["tags"]["tag"]) ? $data["tags"]["tag"] : array ($data["tags"]["tag"]);
        }
        else {
            $taskSeria->tags = array ();
        }
        
        $taskSeria->participants = $data["participants"];
        
        $taskSeria->notes = array ();
        if (isset($data["notes"]["note"])) {
            $data["notes"]["note"] = isset($data["notes"]["note"][0]) ? $data["notes"]["note"] : array ($data["notes"]["note"]);
            
            foreach ($data["notes"]["note"] as $aNote) {
                $taskSeria->notes[] = RtmApiNote::createFromRaw($aNote);
            }
        }
        
        $taskSeria->task = RtmApiTask::createFromRaw($data["task"]);
        
        return $taskSeria;
    }
    
    /**
     * @return string
     */
    public function __toString() {
        $aResult = array ();
        
        if ($this->task->completed != "") {
            $aResult[] = "[√]";
        }
        
        if ($this->task->deleted) {
            $aResult[] = "[x]";
        }
        
        $aResult[] = $this->name;
        
        if ($this->url != "") {
            $aResult[] = "[" . $this->url . "]";
        }
        
        if (!empty($this->tags)) {
            foreach ($this->tags as $tag)
            $aResult[] = "#" . $tag;
        }
        
        if ($this->task->due != "") {
            $timestamp = strtotime($this->task->due);
            $diff = $timestamp - time();
            
            $date = date("Y-m-d", $timestamp);
            if ($date == date("Y-m-d")) {
                $date = "today";
            }
            elseif ($diff > 0 && $diff < 24*3600*7) {
                $date = date("l", $timestamp);
            }
            elseif (date("Y", $timestamp) == date("Y")) {
                $date = date("F j", $timestamp);
            }
            
            $time = date("H:i", $timestamp);
            
            if ($this->task->has_due_time) {
                $aResult[] = "^$date $time";
            }
            else {
                $aResult[] = "^$date";
            }
        }
        
        if ($this->task->estimate != "") {
            $aResult[] = "=" . $this->task->estimate;
        }
        
        if ($this->rrule !== null) {
            $aResult[] = "*" . $this->rrule;
        }
        
        if ($this->task->priority != "N") {
            $aResult[] = "!" . $this->task->priority;
        }
        
        if (!empty($this->notes)) {
            $aResult[] = "\n\n" . implode("\n\n", $this->notes);
        }
        
        $aResult[] = " " . $this->getFullTaskId();
                
        return implode(" ", $aResult);
    }
    
    /**
     * @return string
     */
    public function getFullTaskId() {
        return "#" . $this->listId . "-" . $this->id . "-" . $this->task->id;
    }
}

class RtmApiTask extends RtmApiObject {
    public $id;
    public $due;
    public $has_due_time;
    public $added;
    public $completed;
    public $deleted;
    public $priority;
    public $postponed;
    public $estimate;
    
    /**
     * @param array $data
     * @return RtmApiTask
     */
    public static function createFromRaw($data) {
        $task = new RtmApiTask();
        $task->id = $data["id"];
        $task->due = $data["due"];
        $task->has_due_time = $data["has_due_time"];
        $task->added = $data["added"];
        $task->completed = $data["completed"];
        $task->deleted = $data["deleted"];
        $task->priority = $data["priority"];
        $task->postponed = $data["postponed"];
        $task->estimate = $data["estimate"];
        
        return $task;
    }
}

class RtmApiNote extends RtmApiObject implements Printable {
    public $id;
    public $created;
    public $modified;
    public $title;
    public $text;
    
    /**
     * @param array $data
     * @return RtmApiNote
     */
    public static function createFromRaw($data) {
        $note = new RtmApiNote();
        $note->id = $data["id"];
        $note->created = $data["created"];
        $note->modified = $data["modified"];
        $note->title = $data["title"];
        $note->text = $data["\$t"];
        
        return $note;
    }
    
    /**
     * 
     * @return string
     */
    public function __toString() {
        if ($this->title != "") {
            return $this->title . "\n" . $this->text;
        }
        else {
            return $this->text;
        }
    }
}

class RtmApiRrule extends RtmApiObject implements Printable {
    /**
     * Whether it is 'every' (1) or 'after' (0) rule.
     * 
     * @var int
     */
    public $every;
    
    /**
     * 
     * @var string
     */
    public $rule;
    
    /**
     * @param array $data
     * @return RtmApiRrule
     */
    public static function createFromRaw($data) {
        $rrule = new RtmApiRrule();
        $rrule->every = $data["every"];
        $rrule->rule = $data["\$t"];
        
        return $rrule;
    }
    
    /**
     * 
     * @return string
     */
    public function __toString() {
        $FREQ = "";
        $INTERVAL = "";
        $BYDAY = "";
        $BYMONTHDAY = "";
        
        $aRulePars = explode(";", $this->rule);
        foreach ($aRulePars as $pair) {
            list ($name, $value) = explode("=", $pair);
            $$name = $value;
        }
        
        // make 'monthly' 'month' and 'daily' => 'day'
        $FREQ = preg_replace(array ("`monthly`", "`daily`", "`yearly`", "`weekly`"), array ("month", "day", "year", "week"), strtolower($FREQ));
        
        $s = array ();
        
        if ($this->every == 1) {
            $s[] = "every $INTERVAL " . strtolower($FREQ) . ($INTERVAL > 1 ? "s" : "");
        }
        else {
            $s[] = "after $INTERVAL " . strtolower($FREQ) . ($INTERVAL > 1 ? "s" : "");
        }
        
        if ($BYDAY != "") {
            preg_match("`^(\d)(..)$`", $BYDAY, $aMatches);
            $s[] = "on the " . $this->makeNumeral($aMatches[1]) . " " . $this->makeReadableWeekday($aMatches[2]);
        }
        
        if ($BYMONTHDAY != "") {
            $s[] = "on the " . $this->makeNumeral($BYMONTHDAY);
        }
        
        return implode(" ", $s);
    }
    
    private function makeNumeral($number) {
        $number = (string) $number;
        
        $lastDigit = $number[strlen($number) - 1];
        
        switch ($lastDigit) {
            case "1":
                $number .= "st";
                break;
            case "2":
                $number .= "nd";
                break;
            case "3":
                $number .= "rd";
                break;
            default:
                $number .= "th";
        }
        
        return $number;
    }
    
    private function makeReadableWeekday($name) {
        return preg_replace(
            array ("`MO`", "`TU`", "`WE`", "`TH`", "`FR`", "`SA`", "`SU`"), 
            array (" Monday", " Tuesday", " Wednsday", " Thursday", " Friday", " Saturday", " Sunday"), 
            $name
        );
    }
}

class RtmApiList extends RtmApiObject {
    public $id;
    public $name;
    public $deleted;
    public $locked;
    public $archived;
    public $position;
    public $smart;
    public $filter;
    public $sort_order;
    
    private $aTaskseries = null;
    
    /**
     * @param array $data
     * @return RtmApiList
     */
    public static function createFromRaw($data) {
        $list = new RtmApiList();
        $list->id = $data["id"];
        
        if (isset($data["taskseries"])) {
            $list->aTaskseries = array ();
            
            if (!isset($data["taskseries"][0])) {
                $data["taskseries"] = array ($data["taskseries"]);
            }
            
            foreach ($data["taskseries"] as $taskseriaData) {
                $list->aTaskseries[] = RtmApiTaskseria::createFromRaw($taskseriaData, $list->id);
            }
        }
        else {
            $list->name = $data["name"];
            $list->deleted = $data["deleted"];
            $list->locked = $data["locked"];
            $list->archived = $data["archived"];
            $list->position = $data["position"];
            $list->smart = $data["position"];
            $list->filter = isset($data["filter"]) ? $data["filter"] : null;
            $list->sort_order = $data["sort_order"];
        }
        
        
        return $list;
    }
    
    /**
     * @return RtmApiTaskseria[]
     */
    public function getTaskseries() {
        if ($this->aTaskseries === null) {
            throw new RtmApiException("The list has no attached taskseries");
        }
        else {
            return $this->aTaskseries;
        }
    }
}